#include "Spawn.hpp"
#include <assert.h>
#include <iostream>
#include <windows.h>
#undef THREAD_PRIORITY_NORMAL

#include "MessageQueue.hpp"
#include "StringUtils.hpp"
#include "SpawnInternal.hpp"

namespace EGlib
{
  ///////////
  // Process
  Process::Process(const Glib::ustring &executable_name)
  {
    this->executable_name = executable_name;
  }

  Glib::RefPtr<Process> Process::create(const Glib::ustring &executable_name)
  {
    return Glib::RefPtr<Process>(new Process(executable_name));
  }

  void Process::add_argument(const Glib::ustring &arg)
  {
    args.push_back(arg);
  }

  void Process::set_working_directory(const Glib::ustring &working_dir)
  {
    this->working_dir = working_dir;
  }

  void Process::set_stdin_channel(Glib::RefPtr<Glib::IOChannel> stdin_channel)
  {
    this->stdin_channel = stdin_channel;
  }

  void Process::set_stdout_channel(Glib::RefPtr<Glib::IOChannel> stdout_channel)
  {
    this->stdout_channel = stdout_channel;
  }

  void Process::set_stderr_channel(Glib::RefPtr<Glib::IOChannel> stderr_channel)
  {
    this->stderr_channel = stderr_channel;
  }

  int Process::spawn_sync()
  {
    ProcessRunner runner;
    
    reference();
    Glib::RefPtr<Process> this_ptr(this);

    return runner.spawn_sync(this_ptr);
  }

  ///////////////
  // ProcessRunner
  int ProcessRunner::spawn_sync(Glib::RefPtr<Process> proc)
  {
    this->proc = proc;

    try
    {
      set_up();
      
      start_process();
      handle_pipes();

      DWORD exit_code;
      BOOL res = GetExitCodeProcess(proc_info.hProcess,&exit_code);
      if(!res)
        throw Win32Exception::get_last_error();      

      clean_up();

      return (int)exit_code;
    }
    catch(...)
    {
      clean_up();
      throw;
    }
  }

  void ProcessRunner::start_process()
  {
    Glib::ustring cur_working_dir;
    if(proc->working_dir == "")
      cur_working_dir = Glib::get_current_dir();
    else
      cur_working_dir = proc->working_dir;

    // TODO: escaping!!
    std::wstring cmd_line;
    cmd_line = to_wstring(proc->executable_name);
    for(int i = 0;i < (int)proc->args.size();i++)
    {
      cmd_line += to_wstring(" " + proc->args[i]);
    }

    STARTUPINFO startup_info;
    memset(&startup_info,0,sizeof(STARTUPINFO));
    startup_info.cb = sizeof(STARTUPINFO);

    startup_info.dwFlags |= STARTF_USESTDHANDLES;
    
    if(proc->stdin_channel)
    {
      stdin_pipe = new WritePipe;
      startup_info.hStdInput = stdin_pipe->get_read_handle();
    }
    else
      startup_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE);

    if(proc->stdout_channel)
    {
      stdout_pipe = new ReadPipe;
      startup_info.hStdOutput = stdout_pipe->get_write_handle();
    }
    else
      startup_info.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);

    if(proc->stderr_channel)
    {
      stderr_pipe = new ReadPipe;
      startup_info.hStdError = stderr_pipe->get_write_handle();
    }
    else
      startup_info.hStdError = GetStdHandle(STD_ERROR_HANDLE);

    memset(&proc_info,0,sizeof(proc_info));
    BOOL res = CreateProcess(NULL,(LPWSTR)cmd_line.c_str(),NULL,NULL,
      true,0,NULL,to_wstring(cur_working_dir).c_str(),
      &startup_info,&proc_info);
    if(!res)
    {
      throw Win32Exception::get_last_error();
    }
  }

  void ProcessRunner::handle_pipes()
  {
    while(1)
    {
      HANDLE wait_handles[4];
      HANDLE *writeptr = wait_handles;

      if(stdin_pipe)
        *(writeptr++) = stdin_pipe->get_overlapped_io_done_event();
      if(stdout_pipe)
        *(writeptr++) = stdout_pipe->get_overlapped_io_done_event();
      if(stderr_pipe)
        *(writeptr++) = stderr_pipe->get_overlapped_io_done_event();
      *(writeptr++) = proc_info.hProcess;

      int num_wait_handles = (int)(writeptr - wait_handles);

      DWORD wait_res = WaitForMultipleObjects((DWORD)num_wait_handles,
        wait_handles,FALSE,INFINITE);

      int handle_index = wait_res - WAIT_OBJECT_0;
      assert(handle_index >= 0 && handle_index < num_wait_handles);

      if(stdin_pipe && 
        wait_handles[handle_index] == stdin_pipe->get_overlapped_io_done_event())
      {
        // provide more input data.

        stdin_pipe_write_async();
      }

      if(stdout_pipe &&
        wait_handles[handle_index] == stdout_pipe->get_overlapped_io_done_event())
      {
        if(!read_pipe_output_async(stdout_pipe,stdout_buffer,proc->stdout_channel))
        {
          delete stdout_pipe;
          stdout_pipe = 0;
        }
      }

      if(stderr_pipe && 
        wait_handles[handle_index] == stderr_pipe->get_overlapped_io_done_event())
      {
        if(!read_pipe_output_async(stderr_pipe,stderr_buffer,proc->stderr_channel))
        {
          delete stderr_pipe;
          stderr_pipe = 0;
        }
      }

      if(wait_handles[handle_index] == proc_info.hProcess)
      {
        // the process has shut down.
        break;
      }
    }
  }

  void ProcessRunner::stdin_pipe_write_async()
  {
    // reads data from the proc->stdin_channel, and forwards it to
    // the stdin pipe of the child process.

    gsize bytes_read;

  again:
    Glib::IOStatus status = proc->stdin_channel->read(stdin_buffer,PIPE_BUFFERS_SIZE,bytes_read);
    switch(status)
    {
    case Glib::IO_STATUS_ERROR:
      assert(!"not implemented yet");
      break;

    case Glib::IO_STATUS_NORMAL:
      {
        stdin_pipe->write_overlapped(stdin_buffer,bytes_read);
      }
      break;

    case Glib::IO_STATUS_EOF:
      {
        SetEndOfFile(stdin_pipe->get_write_handle());
        delete stdin_pipe;
        stdin_pipe = 0;
      }
      break;

    case Glib::IO_STATUS_AGAIN:
      goto again;
    }
  }

  bool ProcessRunner::read_pipe_output_async(ReadPipe *pipe,char *buffer,
    Glib::RefPtr<Glib::IOChannel> channel)
  {
    // forward the stuff that has been read to the channel, provided as a param.
    int size_read = pipe->get_read_size();
    if(size_read == -1)
    {
      return false;
    }
    else if(size_read)
    {
      write_to_channel(channel,buffer,size_read);
    }

    // and issue a new overlapped read.

    pipe->read_overlapped(buffer,PIPE_BUFFERS_SIZE);

    return true;
  }

  void ProcessRunner::write_to_channel(Glib::RefPtr<Glib::IOChannel> channel,
    char *buffer,gsize size)
  {
    gsize total_num_written = 0;

    while(total_num_written < size)
    {
      gsize cur_bytes_written;
      Glib::IOStatus status = channel->write(
        buffer + total_num_written,size - total_num_written,cur_bytes_written);
      // doesn't make any sense for writing, doesn't it?
      assert(status != Glib::IO_STATUS_EOF);
      switch(status)
      {
      case Glib::IO_STATUS_ERROR:
        assert(!"not implemented yet");
        break;

      case Glib::IO_STATUS_NORMAL:
        total_num_written += cur_bytes_written;
        break;

      case Glib::IO_STATUS_AGAIN:
        // just try to write the same part again, the next iteration.
        break;
      }
    }
  }

  void ProcessRunner::set_up()
  {
    memset(&proc_info,0,sizeof(proc_info));

    stdin_pipe = 0;
    stdout_pipe = 0;
    stderr_pipe = 0;
  }

  void ProcessRunner::clean_up()
  {
    delete stdin_pipe;
    stdin_pipe = 0;

    delete stdout_pipe;
    stdout_pipe = 0;

    delete stderr_pipe;
    stderr_pipe = 0;

    CloseHandle(proc_info.hThread);
    proc_info.hThread = 0;

    CloseHandle(proc_info.hProcess);
    proc_info.hProcess = 0;
  }
}
